package com.febs.security.helper;

import com.febs.common.domain.FebsConstant;
import com.febs.common.helper.StringRedisHelper;
import com.febs.common.utils.HttpContextUtils;
import com.febs.security.domain.FebsLoginUser;
import com.febs.security.domain.FebsUserDetails;
import com.febs.security.properties.FebsSecurityProperties;
import com.febs.security.properties.TokenProperties;
import com.google.common.collect.Maps;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Date;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;

/**
 * @author zhongyj <1126834403@qq.com><br/>
 * @date 2021/6/21
 */
@Slf4j
@Service
public class FebsJwtTokenHelper {

    @Value("${" + FebsSecurityProperties.ENABLE_REDIS_CACHE + "}")
    private boolean enableRedisCache;

    @Resource
    private FebsSecurityProperties properties;
    @Resource
    private StringRedisHelper redisHelper;
    @Resource
    private SessionStrategy sessionStrategy;

    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public FebsLoginUser getLoginUserFromToken(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = getToken(request);
        if (StrUtil.isNotBlank(token)) {
            Claims claims = getClaimsFromToken(token);
            // 解析对应的权限以及用户信息
            String uuid = (String) claims.get(FebsConstant.LOGIN_USER_KEY);
            return Optional.ofNullable(getCacheToken(uuid)).orElse(getCacheToken(HttpContextUtils.getRequest().getSession().getId()));
        }
        return null;
    }

    public FebsLoginUser getLoginUserFromUuid(String uuid) {
        // 解析对应的权限以及用户信息
        return Optional.ofNullable(getCacheToken(uuid)).orElse(getCacheToken(HttpContextUtils.getRequest().getSession().getId()));
    }


    /**
     * 根据 TokenDetail 生成 Token
     *
     * @param loginUser 用户信息
     * @return 生成的token
     */
    public String generateToken(FebsLoginUser loginUser) {
        Map<String, Object> claims = Maps.newHashMap();
        claims.put("sub", loginUser.getUsername());
        claims.put("created", this.generateCurrentDate());
        claims.put(FebsConstant.LOGIN_USER_KEY, UUID.fastUUID().toString());
        String token = this.generateToken(claims);
        cacheToken(StrUtil.toString(claims.get(FebsConstant.LOGIN_USER_KEY)), JSONUtil.toJsonStr(loginUser));
        return token;
    }

    /**
     * 从 token 中拿到 username
     *
     * @param token 请求头中获取的token
     * @return username
     */
    public String getUsernameFromToken(String token) {
        String username;
        try {
            final Claims claims = this.getClaimsFromToken(token);
            username = claims.getSubject();
        } catch (Exception e) {
            username = null;
        }
        return username;
    }

    /**
     * 检查 token 是否处于有效期内
     *
     * @param token       Token
     * @param userDetails 用户信息
     * @return 验证结果
     */
    public Boolean validateToken(String token, UserDetails userDetails) {
        FebsUserDetails user = (FebsUserDetails) userDetails;
        final String username = this.getUsernameFromToken(token);
        return (username.equals(user.getUsername()) && !(this.isTokenExpired(token)));
    }

    /**
     * 获得我们封装在 token 中的 token 创建时间
     *
     * @param token Token
     * @return Token创建时间
     */
    public Date getCreatedDateFromToken(String token) {
        Date created;
        try {
            final Claims claims = this.getClaimsFromToken(token);
            created = new Date((Long) claims.get("created"));
        } catch (Exception e) {
            created = null;
        }
        return created;
    }

    /**
     * 获得我们封装在 token 中的 uuid
     *
     * @param token Token
     * @return Token创建时间
     */
    public String getUuidFromToken(String token) {
        String uuid;
        try {
            final Claims claims = this.getClaimsFromToken(token);
            uuid = StrUtil.toString(claims.get(FebsConstant.LOGIN_USER_KEY));
        } catch (Exception e) {
            uuid = null;
        }
        return uuid;
    }

    /**
     * 获得我们封装在 token 中的 token 过期时间
     *
     * @param token Token
     * @return Token 的过期时间
     */
    public Date getExpirationDateFromToken(String token) {
        Date expiration;
        try {
            final Claims claims = this.getClaimsFromToken(token);
            expiration = claims.getExpiration();
        } catch (Exception e) {
            expiration = null;
        }
        return expiration;
    }

    /**
     * 获取请求token
     *
     * @param request HttpServletRequest
     * @return token
     */
    private String getToken(HttpServletRequest request) {
        String header = properties.getToken().getHeader();
        String token = request.getHeader(header);
        if (StrUtil.isNotBlank(token) && token.startsWith(FebsConstant.TOKEN_PREFIX)) {
            token = token.replace(FebsConstant.TOKEN_PREFIX, "");
        }
        return token;
    }

    /**
     * 检查当前时间是否在封装在 token 中的过期时间之后，若是，则判定为 token 过期
     *
     * @param token Token
     * @return 是否过期
     */
    private Boolean isTokenExpired(String token) {
        final Date expiration = this.getExpirationDateFromToken(token);
        return expiration.before(this.generateCurrentDate());
    }

    /**
     * 根据 claims 生成 Token
     *
     * @param claims 用户信息
     * @return 生成的token
     */
    private String generateToken(Map<String, Object> claims) {
        TokenProperties tokenProperties = properties.getToken();
        String token;
        try {

            token = Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(this.generateExpirationDate())
                    .signWith(SignatureAlgorithm.HS512, StrUtil.bytes(tokenProperties.getSecret(), "UTF-8"))
                    .compact();
        } catch (Exception ex) {
            //didn't want to have this method throw the exception, would rather log it and sign the token like it was before
            log.warn(ex.getMessage());
            token = Jwts.builder()
                    .setClaims(claims)
                    .setExpiration(this.generateExpirationDate())
                    .signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret())
                    .compact();
        }
        return token;
    }

    /**
     * 缓存用户信息
     *
     * @param key         UUID
     * @param userDetails 用户信息
     */
    private void cacheToken(String key, String userDetails) {
        String tokenKey = FebsConstant.CACHE_KEY_TOKEN_PREFIX + key;
        if (enableRedisCache) {
            redisHelper.setEx(tokenKey, userDetails, properties.getToken().getExpiration(), TimeUnit.MINUTES);
        } else {
            sessionStrategy.setAttribute(new ServletRequestAttributes(HttpContextUtils.getRequest()), tokenKey, userDetails);
        }
    }

    /**
     * 获取用户信息
     *
     * @param key UUID
     * @return Object
     */
    private FebsLoginUser getCacheToken(String key) {
        String tokenKey = FebsConstant.CACHE_KEY_TOKEN_PREFIX + key;
        if (enableRedisCache) {
            return JSONUtil.toBean(redisHelper.get(tokenKey), FebsLoginUser.class);
        } else {
            return JSONUtil.toBean((String) sessionStrategy.getAttribute(new ServletRequestAttributes(HttpContextUtils.getRequest()), tokenKey), FebsLoginUser.class);
        }
    }

    /**
     * 解析 token 的主体 Claims
     *
     * @param token 请求头中获取的token
     * @return 解析出的信息
     */
    private Claims getClaimsFromToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(StrUtil.bytes(getTokenProperties().getSecret(), "UTF-8"))
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }

    /**
     * token 过期时间
     *
     * @return 过期时间
     */
    private Date generateExpirationDate() {
        TokenProperties tokenProperties = properties.getToken();
        return new Date(System.currentTimeMillis() + tokenProperties.getExpiration() * 60 * 1000);
    }

    /**
     * 获得当前时间
     *
     * @return 当前时间
     */
    private Date generateCurrentDate() {
        return new Date(System.currentTimeMillis());
    }

    private TokenProperties getTokenProperties() {
        return this.properties.getToken();
    }
}
